package com.linb.pan.server.modules.file.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.collect.Lists;
import com.linb.pan.core.exception.BPanBusinessException;
import com.linb.pan.core.utils.FileUtil;
import com.linb.pan.core.utils.IdUtil;
import com.linb.pan.server.common.stream.channel.PanChannels;
import com.linb.pan.server.common.stream.event.log.ErrorLogEvent;
import com.linb.pan.server.modules.file.context.FileChunkMergeAndSaveContext;
import com.linb.pan.server.modules.file.context.FileSaveContext;
import com.linb.pan.server.modules.file.entity.BPanFile;
import com.linb.pan.server.modules.file.entity.BPanFileChunk;
import com.linb.pan.server.modules.file.mapper.BPanFileMapper;
import com.linb.pan.server.modules.file.service.IFileChunkService;
import com.linb.pan.server.modules.file.service.IFileService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.linb.pan.storage.engine.core.StorageEngine;
import com.linb.pan.storage.engine.core.context.DeleteFileContext;
import com.linb.pan.storage.engine.core.context.MergeFileContext;
import com.linb.pan.storage.engine.core.context.StoreFileContext;
import com.linb.pan.stream.core.IStreamProducer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;


@Service
@Slf4j
public class FileServiceImpl extends ServiceImpl<BPanFileMapper, BPanFile> implements IFileService {


    @Autowired
    @Qualifier(value = "defaultStreamProducer")
    private IStreamProducer producer;

    @Autowired
    private StorageEngine storageEngine;

    @Autowired
    private IFileChunkService fileChunkService;

    /**
     * 保存文件
     */
    @Override
    public void saveFile(FileSaveContext context) {
        log.info("fileService-saveFile: context = {}", context);
        //1.上传单文件
        storeMultipartFile(context);
        //2.保存到b_pan_file
        BPanFile record = doSaveFile(
                context.getFilename(),
                context.getRealPath(),
                context.getTotalSize(),
                context.getIdentifier(),
                context.getUserId()
        );
        //3.填充record
        context.setRecord(record);
    }

    /**
     * 合并物理文件并且保存到文件表
     */
    @Override
    public void mergeFileChunkAndSaveFile(FileChunkMergeAndSaveContext context) {
        //1.存储引擎物理合并分片文件
        doMergeFileChunk(context);
        //2.保存到文件表
        BPanFile record = doSaveFile(
                context.getFilename(),
                context.getRealPath(),
                context.getTotalSize(),
                context.getIdentifier(),
                context.getUserId()
        );
        context.setRecord(record);
    }

    /*================================private================================*/

    /**
     * 存储引擎物理合并分片文件
     */
    private void doMergeFileChunk(FileChunkMergeAndSaveContext context) {
        //1.查询文件分片记录
        QueryWrapper<BPanFileChunk> queryWrapper = Wrappers.query();
        queryWrapper.eq("identifier", context.getIdentifier());
        queryWrapper.eq("create_user", context.getUserId());
        queryWrapper.ge("expiration_time", new Date());
        List<BPanFileChunk> chunkRecordList = fileChunkService.list(queryWrapper);
        if (CollectionUtils.isEmpty(chunkRecordList)) {
            throw new BPanBusinessException("该文件未找到分片记录");
        }
        //2.根据文件分片的记录去物理合并
        //2.1.提取路径集合
        List<String> realPathList = chunkRecordList.stream()
                .sorted(Comparator.comparing(BPanFileChunk::getChunkNumber))//按照下标升序排序
                .map(BPanFileChunk::getRealPath)
                .collect(Collectors.toList());
        try {
            MergeFileContext mergeFileContext = new MergeFileContext();
            mergeFileContext.setFilename(context.getFilename());
            mergeFileContext.setIdentifier(context.getIdentifier());
            mergeFileContext.setUserId(context.getUserId());
            mergeFileContext.setRealPathList(realPathList);
            storageEngine.mergeFile(mergeFileContext);
            //物理存储完后mergeFileContext会存有realPath
            context.setRealPath(mergeFileContext.getRealPath());
            log.info("合并后文件路径: {}", mergeFileContext.getRealPath());
        } catch (IOException e) {
            e.printStackTrace();
            throw new BPanBusinessException("文件分片合并失败");
        }
        //3.删除文件分片记录
        List<Long> chunkRecordIds = chunkRecordList.stream().map(BPanFileChunk::getId).collect(Collectors.toList());
        fileChunkService.removeByIds(chunkRecordIds);
        //4.封装合并文件的真实存储路径到上下文中
    }

    /**
     * 上传单文件，委托给文件存储系统处理
     */
    private void storeMultipartFile(FileSaveContext context) {
        try {
            log.info("上传单文件到物理磁盘: context = {}", context);
            StoreFileContext storeFileContext = new StoreFileContext();
            storeFileContext.setFilename(context.getFilename());
            storeFileContext.setTotalSize(context.getTotalSize());
            storeFileContext.setInputStream(context.getFile().getInputStream());
            storageEngine.store(storeFileContext);
            context.setRealPath(storeFileContext.getRealPath());
        } catch (IOException e) {
            e.printStackTrace();
            throw new BPanBusinessException("文件上传失败");
        }
    }

    /**
     * 保存文件记录
     */
    private BPanFile doSaveFile(String filename, String realPath, Long totalSize, String identifier, Long userId) {
        BPanFile record = assembleBPanFile(filename, realPath, totalSize, identifier, userId);
        if (!save(record)) {
            try {
                log.error("保存失败，删除已上传的物理文件");
                DeleteFileContext deleteFileContext = new DeleteFileContext();
                deleteFileContext.setRealFilePathList(Lists.newArrayList(realPath));
                storageEngine.delete(deleteFileContext);
            } catch (IOException e) {
                e.printStackTrace();
                ErrorLogEvent event = new ErrorLogEvent("文件物理删除失败，文件路径: " + realPath, userId);
                producer.sendMessage(PanChannels.ERROR_LOG_OUTPUT, event);
            }
        }
        return record;
    }

    /**
     * 转换为实体类
     */
    private BPanFile assembleBPanFile(String filename, String realPath, Long totalSize, String identifier, Long userId) {
        BPanFile record = new BPanFile();
        record.setFileId(IdUtil.get());
        record.setFilename(filename);
        record.setRealPath(realPath);
        record.setFileSize(String.valueOf(totalSize));
        record.setFileSizeDesc(FileUtil.byteCountToDisplaySize(totalSize));
        record.setFileSuffix(FileUtil.getFileSuffix(filename));
        record.setIdentifier(identifier);
        record.setCreateUser(userId);
        record.setCreateTime(new Date());
        log.info("转BPanFile实体: {}", record);
        return record;
    }

}
